AWS CDKでスタック内の全てのEC2インスタンスをEBS最適化インスタンスにしてみた
AWS CDKでデプロイしたEC2インスタンスをEBS最適化インスタンスにしたい
こんにちは、のんピ(@non____97)です。
皆さんはEBS最適化インスタンスは好きですかか? 私は好きです。
EBS最適化インスタンスにすることで、EC2インスタンスとEBSボリューム間に専用のネットワークスループットが確保されます。結果として、EBSボリュームのパフォーマンスを安定的に引き出すことができるようになるありがたい機能です。
AWS公式ドキュメントも「EBS ボリュームの最高のパフォーマンスを実現します。」とかなり熱く紹介していますね。
特にコストなくパフォーマンスを向上できるのであれば、AWS CDKでパパッとEC2インスタンスを作るときもEBS最適化インスタンスとしてデプロイしたいと思うのは当然でしょう。
しかし、L2 ConstructであるInstanceのプロパティを探してもEBS最適化を有効にするような設定はありません。
実際、以下のようなコードでデプロイすると、デプロイされたEC2インスタンスはEBS最適化が無効になっています。
import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib"; import { Construct } from "constructs"; export class Ec2InstanceStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC const vpc = new ec2.Vpc(this, "VPC", { cidr: "10.10.0.0/24", enableDnsHostnames: true, enableDnsSupport: true, natGateways: 0, maxAzs: 2, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 }, ], }); // EC2 Instance new ec2.Instance(this, "EC2 Instance", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc: vpc, blockDevices: [ { deviceName: "/dev/xvda", volume: ec2.BlockDeviceVolume.ebs(8, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); } }
これは困った
ということで、AWS CDKでEC2インスタンスをEBS最適化インスタンスとしてデプロイする方法を色々考えてみました。
CloudFormationとL1 ConstructのEBS最適化設定の仕方
そもそもCloudFormationでEBS最適化設定が出来るのか確認します。
AWS::EC2::Instanceを確認すると、EbsOptimized
というプロパティがあることから、CloudFormationでは設定出来るようですね。
Type: AWS::EC2::Instance Properties: AdditionalInfo: String Affinity: String AvailabilityZone: String BlockDeviceMappings: - BlockDeviceMapping CpuOptions: CpuOptions CreditSpecification: CreditSpecification DisableApiTermination: Boolean EbsOptimized: Boolean ElasticGpuSpecifications: - ElasticGpuSpecification ElasticInferenceAccelerators: - ElasticInferenceAccelerator EnclaveOptions: EnclaveOptions HibernationOptions: HibernationOptions HostId: String HostResourceGroupArn: String IamInstanceProfile: String ImageId: String InstanceInitiatedShutdownBehavior: String InstanceType: String Ipv6AddressCount: Integer Ipv6Addresses: - InstanceIpv6Address KernelId: String KeyName: String LaunchTemplate: LaunchTemplateSpecification LicenseSpecifications: - LicenseSpecification Monitoring: Boolean NetworkInterfaces: - NetworkInterface PlacementGroupName: String PrivateDnsNameOptions: PrivateDnsNameOptions PrivateIpAddress: String PropagateTagsToVolumeOnCreation: Boolean RamdiskId: String SecurityGroupIds: - String SecurityGroups: - String SourceDestCheck: Boolean SsmAssociations: - SsmAssociation SubnetId: String Tags: - Tag Tenancy: String UserData: String Volumes: - Volume
次に、L1 Constructであるclass CfnInstanceのプロパティも確認してみましょう。
確かにebsOptimized
というプロパティがありますね。
ebsOptimized?
Type: boolean | IResolvable (optional)
Indicates whether the instance is optimized for Amazon EBS I/O.
This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal Amazon EBS I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS-optimized instance.
Default: false
L2 Constructのプロパティを上書きする
L1 ConstructではEBS最適化設定が出来ることが分かったので、L1 Constructを使いましょう
というのは私はちょっと嫌です。
プロパティが少し足りないからといってL1 Constructを使うというのは、AWS CDKを使う旨味が少なくなる気がします。
対応として、L2 Constructでインスタンスを作成してからプロパティを上書きします。
こちらの手法はAWS公式ドキュメントでも紹介されています。
実際のコードは以下の通りです。
import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib"; import { Construct } from "constructs"; export class Ec2InstanceStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC const vpc = new ec2.Vpc(this, "VPC", { cidr: "10.10.0.0/24", enableDnsHostnames: true, enableDnsSupport: true, natGateways: 0, maxAzs: 2, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 }, ], }); // EC2 Instance const ec2Instance = new ec2.Instance(this, "EC2 Instance", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc: vpc, blockDevices: [ { deviceName: "/dev/xvda", volume: ec2.BlockDeviceVolume.ebs(8, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); const cfnInstance = ec2Instance.node.defaultChild as ec2.CfnInstance; cfnInstance.ebsOptimized = true; } }
こちらのコードでデプロイしてみると、EC2インスタンスのEBS最適化が有効になりました。
できらぁ!
スタック内の全てのEC2インスタンスをまとめて設定する
複数のEC2インスタンスをスタック内で定義している場合に面倒だな
EC2インスタンスが数台しかない場合は、上述した方法で対応するのも良いでしょう。
しかし、EC2インスタンスが複数ある場合に都度設定するのもちょっと面倒です。
ということで、以下2つの方法で複数のEC2インスタンスをまとめてEBS最適化インスタンスにしてみます。
- スタックの
node.children
からCfnInstance
のノードを探してプロパティを上書きする - EBS最適化インスタンス用のカスタムコンストラクトを作成する
1. スタックの"node.children"から"CfnInstance"のノードを探してプロパティを上書きする
まず、1つ目の方法です。
./lib/ec2-instance-stack.ts
内にconsole.log(this.node.children)
を追加します。そしてnpx cdk synth
をし、スタックのコンストラクトノード配下を表示してニヤニヤしてみましょう。
出力結果がかなり長いので折りたたみます。
console.log(this.node.children)の結果
[ <ref *1> Vpc { node: Node { host: [Circular *1], _locked: false, _children: [Object], _context: {}, _metadata: [], _dependencies: Set(0) {}, _validations: [], id: 'VPC', scope: [Ec2InstanceStack] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, env: { account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}' }, _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.201]}', natDependencies: [], incompleteSubnetDefinition: false, publicSubnets: [ [PublicSubnet], [PublicSubnet] ], privateSubnets: [], isolatedSubnets: [], subnetConfiguration: [ [Object] ], _internetConnectivityEstablished: DependencyGroup { _deps: [Array], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, networkBuilder: NetworkBuilder { subnetCidrs: [Array], networkCidr: [CidrBlock], nextAvailableIp: 168427552 }, dnsHostnamesEnabled: true, dnsSupportEnabled: true, internetConnectivityEstablished: DependencyGroup { _deps: [Array], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, resource: CfnVPC { node: [Node], stack: [Ec2InstanceStack], logicalId: '${Token[Ec2InstanceStack.VPC.Resource.LogicalID.202]}', cfnOptions: [Object], rawOverrides: {}, dependsOn: Set(0) {}, cfnResourceType: 'AWS::EC2::VPC', _cfnProperties: [Object], attrCidrBlock: '${Token[TOKEN.203]}', attrCidrBlockAssociations: [Array], attrDefaultNetworkAcl: '${Token[TOKEN.205]}', attrDefaultSecurityGroup: '${Token[TOKEN.206]}', attrIpv6CidrBlocks: [Array], attrVpcId: '${Token[TOKEN.208]}', cidrBlock: '10.10.0.0/24', enableDnsHostnames: true, enableDnsSupport: true, instanceTenancy: 'default', ipv4IpamPoolId: undefined, ipv4NetmaskLength: undefined, tags: [TagManager], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, vpcDefaultNetworkAcl: '${Token[TOKEN.205]}', vpcCidrBlockAssociations: [ '#{Token[TOKEN.204]}' ], vpcCidrBlock: '${Token[TOKEN.203]}', vpcDefaultSecurityGroup: '${Token[TOKEN.206]}', vpcIpv6CidrBlocks: [ '#{Token[TOKEN.207]}' ], availabilityZones: [ '${Token[TOKEN.210]}', '${Token[TOKEN.212]}' ], vpcId: '${Token[TOKEN.213]}', vpcArn: 'arn:${Token[AWS.Partition.10]}:ec2:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:vpc/${Token[TOKEN.213]}', internetGatewayId: '${Token[TOKEN.248]}', [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *2> Instance { node: Node { host: [Circular *2], _locked: false, _children: [Object], _context: {}, _metadata: [], _dependencies: Set(0) {}, _validations: [], id: 'EC2 Instance Amazon Linux 2', scope: [Ec2InstanceStack], _defaultChild: [CfnInstance] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, env: { account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}' }, _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.252]}', securityGroups: [ [SecurityGroup] ], securityGroup: SecurityGroup { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.253]}', canInlineRule: false, connections: [Connections], peerAsTokenCount: 0, directIngressRules: [], directEgressRules: [Array], allowAllOutbound: true, disableInlineRules: false, securityGroup: [CfnSecurityGroup], securityGroupId: '${Token[TOKEN.255]}', securityGroupVpcId: '${Token[TOKEN.256]}', securityGroupName: '${Token[TOKEN.257]}', [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, connections: <ref *3> Connections { _securityGroups: [ReactiveList], _securityGroupRules: [ReactiveList], skip: false, remoteRule: false, connections: [Circular *3], defaultPort: undefined }, role: <ref *4> Role { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.258]}', grantPrincipal: [Circular *4], principalAccount: '${Token[AWS.AccountId.7]}', assumeRoleAction: 'sts:AssumeRole', managedPolicies: [], attachedPolicies: [AttachedPolicies], dependables: Map(0) {}, _didSplit: false, assumeRolePolicy: [PolicyDocument], inlinePolicies: {}, permissionsBoundary: undefined, roleId: '${Token[TOKEN.263]}', roleArn: '${Token[TOKEN.264]}', roleName: '${Token[TOKEN.266]}', policyFragment: [PrincipalPolicyFragment], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, grantPrincipal: <ref *4> Role { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.258]}', grantPrincipal: [Circular *4], principalAccount: '${Token[AWS.AccountId.7]}', assumeRoleAction: 'sts:AssumeRole', managedPolicies: [], attachedPolicies: [AttachedPolicies], dependables: Map(0) {}, _didSplit: false, assumeRolePolicy: [PolicyDocument], inlinePolicies: {}, permissionsBoundary: undefined, roleId: '${Token[TOKEN.263]}', roleArn: '${Token[TOKEN.264]}', roleName: '${Token[TOKEN.266]}', policyFragment: [PrincipalPolicyFragment], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, userData: LinuxUserData { props: {}, lines: [], onExitLines: [] }, instance: CfnInstance { node: [Node], stack: [Ec2InstanceStack], logicalId: '${Token[Ec2InstanceStack.EC2.Instance.Amazon.Linux.2.Resource.LogicalID.275]}', cfnOptions: [Object], rawOverrides: {}, dependsOn: Set(0) {}, cfnResourceType: 'AWS::EC2::Instance', _cfnProperties: [Object], attrAvailabilityZone: '${Token[TOKEN.276]}', attrPrivateDnsName: '${Token[TOKEN.277]}', attrPrivateIp: '${Token[TOKEN.278]}', attrPublicDnsName: '${Token[TOKEN.279]}', attrPublicIp: '${Token[TOKEN.280]}', additionalInfo: undefined, affinity: undefined, availabilityZone: '${Token[TOKEN.210]}', blockDeviceMappings: [Array], cpuOptions: undefined, creditSpecification: undefined, disableApiTermination: undefined, ebsOptimized: undefined, elasticGpuSpecifications: undefined, elasticInferenceAccelerators: undefined, enclaveOptions: undefined, hibernationOptions: undefined, hostId: undefined, hostResourceGroupArn: undefined, iamInstanceProfile: '${Token[TOKEN.274]}', imageId: '${Token[TOKEN.270]}', instanceInitiatedShutdownBehavior: undefined, instanceType: 't3.micro', ipv6AddressCount: undefined, ipv6Addresses: undefined, kernelId: undefined, keyName: undefined, launchTemplate: undefined, licenseSpecifications: undefined, monitoring: undefined, networkInterfaces: undefined, placementGroupName: undefined, privateDnsNameOptions: undefined, privateIpAddress: undefined, propagateTagsToVolumeOnCreation: true, ramdiskId: undefined, securityGroupIds: [Array], securityGroups: undefined, sourceDestCheck: undefined, ssmAssociations: undefined, subnetId: '${Token[TOKEN.222]}', tags: [TagManager], tenancy: undefined, userData: '${Token[TOKEN.272]}', volumes: undefined, _logicalIdOverride: '${Token[TOKEN.282]}', [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, osType: 0, instanceId: '${Token[TOKEN.281]}', instanceAvailabilityZone: '${Token[TOKEN.276]}', instancePrivateDnsName: '${Token[TOKEN.277]}', instancePrivateIp: '${Token[TOKEN.278]}', instancePublicDnsName: '${Token[TOKEN.279]}', instancePublicIp: '${Token[TOKEN.280]}', [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *5> CfnParameter { node: Node { host: [Circular *5], _locked: false, _children: {}, _context: {}, _metadata: [Array], _dependencies: Set(0) {}, _validations: [], id: 'SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter', scope: [Ec2InstanceStack] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, logicalId: '${Token[Ec2InstanceStack.SsmParameterValue:--aws--servi...:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter.LogicalID.269]}', _type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>', _default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2', _allowedPattern: undefined, _allowedValues: undefined, _constraintDescription: undefined, _description: undefined, _maxLength: undefined, _maxValue: undefined, _minLength: undefined, _minValue: undefined, _noEcho: undefined, [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *6> Import { node: Node { host: [Circular *6], _locked: false, _children: {}, _context: {}, _metadata: [], _dependencies: Set(0) {}, _validations: [], id: 'SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118', scope: [Ec2InstanceStack] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, env: { account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}' }, _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.271]}', parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2', parameterArn: 'arn:${Token[AWS.Partition.10]}:ssm:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:parameter/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2', parameterType: 'AWS::EC2::Image::Id', stringValue: '${Token[TOKEN.270]}', [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *7> Instance { node: Node { host: [Circular *7], _locked: false, _children: [Object], _context: {}, _metadata: [], _dependencies: Set(0) {}, _validations: [], id: 'EC2 Instance Windows Server 2022', scope: [Ec2InstanceStack], _defaultChild: [CfnInstance] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, env: { account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}' }, _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.283]}', securityGroups: [ [SecurityGroup] ], securityGroup: SecurityGroup { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.284]}', canInlineRule: false, connections: [Connections], peerAsTokenCount: 0, directIngressRules: [], directEgressRules: [Array], allowAllOutbound: true, disableInlineRules: false, securityGroup: [CfnSecurityGroup], securityGroupId: '${Token[TOKEN.286]}', securityGroupVpcId: '${Token[TOKEN.287]}', securityGroupName: '${Token[TOKEN.288]}', [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, connections: <ref *8> Connections { _securityGroups: [ReactiveList], _securityGroupRules: [ReactiveList], skip: false, remoteRule: false, connections: [Circular *8], defaultPort: undefined }, role: <ref *9> Role { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.289]}', grantPrincipal: [Circular *9], principalAccount: '${Token[AWS.AccountId.7]}', assumeRoleAction: 'sts:AssumeRole', managedPolicies: [], attachedPolicies: [AttachedPolicies], dependables: Map(0) {}, _didSplit: false, assumeRolePolicy: [PolicyDocument], inlinePolicies: {}, permissionsBoundary: undefined, roleId: '${Token[TOKEN.294]}', roleArn: '${Token[TOKEN.295]}', roleName: '${Token[TOKEN.297]}', policyFragment: [PrincipalPolicyFragment], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, grantPrincipal: <ref *9> Role { node: [Node], stack: [Ec2InstanceStack], env: [Object], _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.289]}', grantPrincipal: [Circular *9], principalAccount: '${Token[AWS.AccountId.7]}', assumeRoleAction: 'sts:AssumeRole', managedPolicies: [], attachedPolicies: [AttachedPolicies], dependables: Map(0) {}, _didSplit: false, assumeRolePolicy: [PolicyDocument], inlinePolicies: {}, permissionsBoundary: undefined, roleId: '${Token[TOKEN.294]}', roleArn: '${Token[TOKEN.295]}', roleName: '${Token[TOKEN.297]}', policyFragment: [PrincipalPolicyFragment], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, userData: WindowsUserData { lines: [], onExitLines: [] }, instance: CfnInstance { node: [Node], stack: [Ec2InstanceStack], logicalId: '${Token[Ec2InstanceStack.EC2.Instance.Windows.Server.2022.Resource.LogicalID.306]}', cfnOptions: [Object], rawOverrides: {}, dependsOn: Set(0) {}, cfnResourceType: 'AWS::EC2::Instance', _cfnProperties: [Object], attrAvailabilityZone: '${Token[TOKEN.307]}', attrPrivateDnsName: '${Token[TOKEN.308]}', attrPrivateIp: '${Token[TOKEN.309]}', attrPublicDnsName: '${Token[TOKEN.310]}', attrPublicIp: '${Token[TOKEN.311]}', additionalInfo: undefined, affinity: undefined, availabilityZone: '${Token[TOKEN.210]}', blockDeviceMappings: [Array], cpuOptions: undefined, creditSpecification: undefined, disableApiTermination: undefined, ebsOptimized: undefined, elasticGpuSpecifications: undefined, elasticInferenceAccelerators: undefined, enclaveOptions: undefined, hibernationOptions: undefined, hostId: undefined, hostResourceGroupArn: undefined, iamInstanceProfile: '${Token[TOKEN.305]}', imageId: '${Token[TOKEN.301]}', instanceInitiatedShutdownBehavior: undefined, instanceType: 't3.micro', ipv6AddressCount: undefined, ipv6Addresses: undefined, kernelId: undefined, keyName: undefined, launchTemplate: undefined, licenseSpecifications: undefined, monitoring: undefined, networkInterfaces: undefined, placementGroupName: undefined, privateDnsNameOptions: undefined, privateIpAddress: undefined, propagateTagsToVolumeOnCreation: true, ramdiskId: undefined, securityGroupIds: [Array], securityGroups: undefined, sourceDestCheck: undefined, ssmAssociations: undefined, subnetId: '${Token[TOKEN.222]}', tags: [TagManager], tenancy: undefined, userData: '${Token[TOKEN.303]}', volumes: undefined, _logicalIdOverride: '${Token[TOKEN.313]}', [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, osType: 1, instanceId: '${Token[TOKEN.312]}', instanceAvailabilityZone: '${Token[TOKEN.307]}', instancePrivateDnsName: '${Token[TOKEN.308]}', instancePrivateIp: '${Token[TOKEN.309]}', instancePublicDnsName: '${Token[TOKEN.310]}', instancePublicIp: '${Token[TOKEN.311]}', [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *10> CfnParameter { node: Node { host: [Circular *10], _locked: false, _children: {}, _context: {}, _metadata: [Array], _dependencies: Set(0) {}, _validations: [], id: 'SsmParameterValue:--aws--service--ami-windows-latest--Windows_Server-2022-English-Full-Base:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter', scope: [Ec2InstanceStack] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, logicalId: '${Token[Ec2InstanceStack.SsmParameterValue:--aws--servi...:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter.LogicalID.300]}', _type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>', _default: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base', _allowedPattern: undefined, _allowedValues: undefined, _constraintDescription: undefined, _description: undefined, _maxLength: undefined, _maxValue: undefined, _minLength: undefined, _minValue: undefined, _noEcho: undefined, [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } }, <ref *11> Import { node: Node { host: [Circular *11], _locked: false, _children: {}, _context: {}, _metadata: [], _dependencies: Set(0) {}, _validations: [], id: 'SsmParameterValue:--aws--service--ami-windows-latest--Windows_Server-2022-English-Full-Base:C96584B6-F00A-464E-AD19-53AFF4B05118', scope: [Ec2InstanceStack] }, stack: Ec2InstanceStack { node: [Node], _missingContext: [], _stackDependencies: {}, templateOptions: {}, _logicalIds: [LogicalIDs], account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}', environment: 'aws://unknown-account/unknown-region', terminationProtection: undefined, _stackName: 'Ec2InstanceStack', tags: [TagManager], artifactId: 'Ec2InstanceStack', templateFile: 'Ec2InstanceStack.template.json', _versionReportingEnabled: true, synthesizer: [DefaultStackSynthesizer], [Symbol(@aws-cdk/core.DependableTrait)]: [Object] }, env: { account: '${Token[AWS.AccountId.7]}', region: '${Token[AWS.Region.11]}' }, _physicalName: undefined, _allowCrossEnvironment: false, physicalName: '${Token[TOKEN.302]}', parameterName: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base', parameterArn: 'arn:${Token[AWS.Partition.10]}:ssm:${Token[AWS.Region.11]}:${Token[AWS.AccountId.7]}:parameter/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base', parameterType: 'AWS::EC2::Image::Id', stringValue: '${Token[TOKEN.301]}', [Symbol(@aws-cdk/core.DependableTrait)]: { dependencyRoots: [Array] } } ]
ニヤニヤしていると、EC2インスタンスを表すノードはdefaultChild
の型がCfnInstance
であることが分かります。
これを活用して、スタックのコンストラクトノード配下でdefaultChild
の型がCfnInstance
のノードのプロパティを更新してあげます。
import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib"; import { Construct } from "constructs"; export class Ec2InstanceStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC const vpc = new ec2.Vpc(this, "VPC", { cidr: "10.10.0.0/24", enableDnsHostnames: true, enableDnsSupport: true, natGateways: 0, maxAzs: 2, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 }, ], }); // EC2 Instance new ec2.Instance(this, "EC2 Instance Amazon Linux 2", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc: vpc, blockDevices: [ { deviceName: "/dev/xvda", volume: ec2.BlockDeviceVolume.ebs(8, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); new ec2.Instance(this, "EC2 Instance Windows Server 2022", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestWindows( ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE ), vpc: vpc, blockDevices: [ { deviceName: "/dev/sda1", volume: ec2.BlockDeviceVolume.ebs(30, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); // Enable EBS Optimized this.node.children.forEach((child) => { if (child.node.defaultChild instanceof ec2.CfnInstance) { child.node.defaultChild.ebsOptimized = true; } }); } }
デプロイ後、EC2インスタンスがEBS最適化インスタンスになっているかAWS CLIで確認してみます。
aws ec2 describe-instances \ --filters Name=instance-state-name,Values=running \ | jq -r ".Reservations[].Instances[] | [.InstanceId, .EbsOptimized, .Tags]" [ "i-0b8a53f3c266a70b6", true, [ { "Key": "aws:cloudformation:stack-id", "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7" }, { "Key": "aws:cloudformation:stack-name", "Value": "Ec2InstanceStack" }, { "Key": "aws:cloudformation:logical-id", "Value": "EC2InstanceAmazonLinux26836F6B7" }, { "Key": "Name", "Value": "Ec2InstanceStack/EC2 Instance Amazon Linux 2" } ] ] [ "i-0a152d994af257d05", true, [ { "Key": "aws:cloudformation:logical-id", "Value": "EC2InstanceWindowsServer20227EF538A3" }, { "Key": "Name", "Value": "Ec2InstanceStack/EC2 Instance Windows Server 2022" }, { "Key": "aws:cloudformation:stack-name", "Value": "Ec2InstanceStack" }, { "Key": "aws:cloudformation:stack-id", "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7" } ] ]
いずれのEC2インスタンスもEbsOptimized
がtrue
になっていますね。
できらぁ!ということです。
2. EBS最適化インスタンスのカスタムコンストラクトを定義する
1つ目の方法でもスタック内の全てのEC2インスタンスをEBS最適化インスタンスに出来ることが分かりました。
しかし、複数のスタックでEC2インスタンスを管理している場合は何だか忘れそうですよね?
ということで、EBS最適化インスタンスのカスタムコンストラクトを定義して、EC2インスタンスを定義する際はカスタムコンストラクトを使用するようにします。
作成したカスタムコンストラクトは以下の通りです。ebsOptimized
をtrue
にするだけの非常にシンプルなものです。
import { aws_ec2 as ec2 } from "aws-cdk-lib"; import { Construct } from "constructs"; export class EbsOptimizedEc2Instance extends ec2.Instance { constructor(scope: Construct, id: string, props: ec2.InstanceProps) { super(scope, id, props); const cfnInstance = this.node.defaultChild as ec2.CfnInstance; cfnInstance.ebsOptimized = true; } }
スタック側では作成したカスタムコンストラクトをインポートし、EC2インスタンス定義時にカスタムコンストラクトを使うように指定します。
import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib"; import { Construct } from "constructs"; import { EbsOptimizedEc2Instance } from "./ebs-optimized-ec2-Instance"; export class Ec2InstanceStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC const vpc = new ec2.Vpc(this, "VPC", { cidr: "10.10.0.0/24", enableDnsHostnames: true, enableDnsSupport: true, natGateways: 0, maxAzs: 2, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 28 }, ], }); // EC2 Instance new EbsOptimizedEc2Instance(this, "EC2 Instance Amazon Linux 2", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc: vpc, blockDevices: [ { deviceName: "/dev/xvda", volume: ec2.BlockDeviceVolume.ebs(8, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); new EbsOptimizedEc2Instance(this, "EC2 Instance Windows Server 2022", { instanceType: new ec2.InstanceType("t3.micro"), machineImage: ec2.MachineImage.latestWindows( ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE ), vpc: vpc, blockDevices: [ { deviceName: "/dev/sda1", volume: ec2.BlockDeviceVolume.ebs(30, { volumeType: ec2.EbsDeviceVolumeType.GP3, }), }, ], propagateTagsToVolumeOnCreation: true, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, }), }); } }
npx cdk diff
で差分を確認しましたが、差分はないようですね。
> npx cdk diff Stack Ec2InstanceStack There were no differences
できらぁ!
と、npx cdk diff
して差分がないことを確認しただけで高らかに宣言するのも違う気がするので、一度EC2インスタンスを削除して再度デプロイしてみます。
デプロイ後、EC2インスタンスがEBS最適化インスタンスになっているかAWS CLIで確認してみます。
aws ec2 describe-instances \ --filters Name=instance-state-name,Values=running \ | jq -r ".Reservations[].Instances[] | [.InstanceId, .EbsOptimized, .Tags]" [ "i-0038ef076fb3b8f04", true, [ { "Key": "aws:cloudformation:stack-id", "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7" }, { "Key": "aws:cloudformation:logical-id", "Value": "EC2InstanceAmazonLinux26836F6B7" }, { "Key": "aws:cloudformation:stack-name", "Value": "Ec2InstanceStack" }, { "Key": "Name", "Value": "Ec2InstanceStack/EC2 Instance Amazon Linux 2" } ] ] [ "i-0e2a59f2be24abc90", true, [ { "Key": "aws:cloudformation:stack-name", "Value": "Ec2InstanceStack" }, { "Key": "aws:cloudformation:stack-id", "Value": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/Ec2InstanceStack/9a5bb440-e794-11ec-9e78-0a54ab21e0b7" }, { "Key": "Name", "Value": "Ec2InstanceStack/EC2 Instance Windows Server 2022" }, { "Key": "aws:cloudformation:logical-id", "Value": "EC2InstanceWindowsServer20227EF538A3" } ] ]
いずれのEC2インスタンスもEbsOptimized
がtrue
になっていますね。
できらぁ!ということです。
L2 Contructのプロパティが足りなくても何とかなります
AWS CDKでスタック内の全てのEC2インスタンスをEBS最適化インスタンスにしてみました。
運良くEBS最適化設定はL1 Constructにプロパティがありました。もし、L1 Constructにもプロパティがない場合はrawオーバーライドかカスタムリソースを使うことになります。いずれの方法も以下AWS公式ドキュメントで紹介されているので、ご参照ください。
また、今回使用したコードは以下リポジトリに保存しています。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!